using System;
using System.Globalization;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Collections.Generic;

namespace CodeSnippet.Formats.Base
{
    /// <summary>
    /// Provides a base class for formatting most programming languages.
    /// </summary>
    public abstract class CodeFormat : SourceFormat
    {
        #region Enumerations
        /// <summary>
        /// Types of regular expression match groups supported.
        /// </summary>
        /// <remarks>
        /// These types are directly related to the classes used in the style sheets.
        /// </remarks>
        private enum DisplayTypes
        {
            Rem,        // Comment
            Str,        // String
            PreProc,    // preprocessor
            Kwrd,       // keyword
            Cls,        // class
            Attr        // Attribute
        }
        #endregion Enumerations

        #region Properties

        #region Regular Expressions
        /// <summary>
        /// Can be overriden to provide a list of regular expression attributes.
        /// </summary>
        protected virtual string AttributesRegex
        {
            get { return string.Empty; }
        }
        /// <summary>
        /// Must be overridden to provide a regular expression string
        /// to match class names.
        /// </summary>
        protected virtual string ClassRegex
        {
            get { return string.Empty; }
        }
        /// <summary>
        /// Must be overridden to provide a regular expression string
        /// to match comments. 
        /// </summary>
        protected abstract string CommentRegex { get; }
        /// <summary>
        /// Must be overridden to provide a regular expression string
        /// to match strings literals. 
        /// </summary>
        protected abstract string StringRegex { get; }
        #endregion Regular Expressions

        #region Definition
        /// <summary>
        /// Determines if the language is case sensitive.
        /// </summary>
        /// <value><b>true</b> if the language is case sensitive, <b>false</b> 
        /// otherwise. The default is true.</value>
        /// <remarks>
        /// A case-insensitive language formatter must override this 
        /// property to return false.
        /// </remarks>
        public virtual bool CaseSensitive
        {
            get { return true; }
        }
        /// <summary>
        /// Must be overridden to provide a list of keywords defined in 
        /// each language.
        /// </summary>
        /// <remarks>
        /// Keywords must be separated with spaces.
        /// </remarks>
        protected abstract string Keywords { get; }
        /// <summary>
        /// Can be overridden to provide a list of preprocessors defined in 
        /// each language.
        /// </summary>
        /// <remarks>
        /// Preprocessors must be separated with spaces.
        /// </remarks>
        protected virtual string Preprocessors
        {
            get { return string.Empty; }
        }
        #endregion Definition

        #endregion Properties

        #region Initialization
        /// <summary>
        /// Initializes this instance code regular expression.
        /// </summary>
        protected override Regex ConstructCodeRegex()
        {
            // Generate the keyword and preprocessor regexes from the keyword lists.
            var r = new Regex(@"\w+-\w+-\w+|\w+-\w+|\w+|-\w+|#\w+|@@\w+|#(?:\\(?:s|w)(?:\*|\+)?\w+)+|@\\w\*+");
            var regKeyword = r.Replace(Keywords, @"(?<=^|\W)$0(?=\W)");
            var regPreproc = r.Replace(Preprocessors, @"(?<=^|\s)$0(?=\s|$)");
            r = new Regex(@" +");
            regKeyword = r.Replace(regKeyword, @"|");
            regPreproc = r.Replace(regPreproc, @"|");

            // Build a master regex with capturing groups.
            var groups = new List<string>();
            AppendGroup(groups, DisplayTypes.Rem.ToString().ToLower(CultureInfo.CurrentCulture), CommentRegex);
            AppendGroup(groups, DisplayTypes.Str.ToString().ToLower(CultureInfo.CurrentCulture), StringRegex);
            AppendGroup(groups, DisplayTypes.PreProc.ToString().ToLower(CultureInfo.CurrentCulture), regPreproc);
            AppendGroup(groups, DisplayTypes.Kwrd.ToString().ToLower(CultureInfo.CurrentCulture), regKeyword);
            AppendGroup(groups, DisplayTypes.Cls.ToString().ToLower(CultureInfo.CurrentCulture), ClassRegex);
            AppendGroup(groups, DisplayTypes.Attr.ToString().ToLower(CultureInfo.CurrentCulture), AttributesRegex);

            var caseInsensitive = CaseSensitive ? 0 : RegexOptions.IgnoreCase;
            return new Regex(string.Join("|", groups.ToArray()), RegexOptions.Singleline | caseInsensitive);
        }
        /// <summary>
        /// Create a named group.
        /// </summary>
        /// <param name="groups">List of groups to add it to.</param>
        /// <param name="groupName">Name of the group if pattern exists.</param>
        /// <param name="commentRegEx">Pattern to use.</param>
        private static void AppendGroup(ICollection<string> groups, string groupName, string commentRegEx)
        {
            if (!string.IsNullOrEmpty(commentRegEx))
            {
                groups.Add(string.Format(CultureInfo.CurrentCulture, @"(?<{0}>{1})", groupName, commentRegEx));
            }
        }
        #endregion Initialization

        #region Matching
        /// <summary>
        /// Called to evaluate the HTML fragment corresponding to each 
        /// matching token in the code.
        /// </summary>
        /// <param name="match">The <see cref="Match"/> resulting from a 
        /// single regular expression match.</param>
        /// <returns>A string containing the HTML code fragment.</returns>
        protected override string MatchEval(Match match)
        {
            if (match.Groups[DisplayTypes.Rem.ToString().ToLower(CultureInfo.CurrentCulture)].Success)
            {
                var reader = new StringReader(match.ToString());
                string line;
                var sb = new StringBuilder();
                while ((line = reader.ReadLine()) != null)
                {
                    if (sb.Length > 0)
                    {
                        sb.AppendLine();
                    }
                    PushOnSpan(sb, DisplayTypes.Rem.ToString().ToLower(CultureInfo.CurrentCulture), line);
                }
                return sb.ToString();
            }

            return CheckForMatchAndProcess(match);
        }
        /// <summary>
        /// Checks for matches and processes them.
        /// </summary>
        /// <param name="match">The match.</param>
        /// <returns></returns>
        private static string CheckForMatchAndProcess(Match match)
        {
            foreach (DisplayTypes dt in Enum.GetValues(typeof(DisplayTypes)))
            {
                if (match.Groups[dt.ToString().ToLower(CultureInfo.CurrentCulture)].Success)
                {
                    return string.Format(CultureInfo.CurrentCulture, "<span class=\"{0}\">{1}</span>", dt.ToString().ToLower(CultureInfo.CurrentCulture), match);
                }
            }
            return string.Empty;
        }
        /// <summary>
        /// Adds a <c>span</c> definition to the specified <see cref="StringBuilder"/> instance.
        /// </summary>
        /// <param name="sb">The buffer where to push the <c>span</c> definition.</param>
        /// <param name="what">What to pust.</param>
        /// <param name="data">Data associated with the <c>what</c>.</param>
        private static void PushOnSpan(StringBuilder sb, string what, string data)
        {
            sb.AppendFormat("<span class=\"{0}\">{1}</span>", what, data);
        }
        #endregion Matching

        #region Overrideables
        /// <summary>
        /// Used to help determine the likelihood that the given <c>source</c>
        /// matches a the format.
        /// </summary>
        /// <param name="source">Content to use for keyword matching.</param>
        /// <returns>Number of matches found in the given source.</returns>
        public override int MatchKeywordCount(string source)
        {
            var r = new Regex(@"\w+-\w+-\w+|\w+-\w+|\w+|-\w+|#\w+|@@\w+|#(?:\\(?:s|w)(?:\*|\+)?\w+)+|@\\w\*+");
            var regKeyword = r.Replace(Keywords, @"(?<=^|\W)$0(?=\W)");
            r = new Regex(@" +");
            regKeyword = r.Replace(regKeyword, @"|");
            var caseInsensitive = CaseSensitive ? 0 : RegexOptions.IgnoreCase;
            r = new Regex(regKeyword, RegexOptions.Singleline | caseInsensitive);
            return r.Matches(source).Count;
        }
        #endregion Overrideables
    }
}